﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using X3DXNA.interfaces;
using Microsoft.Xna.Framework;
using X3DXNA.customTypes;
using X3DXNA.X3DSchema;
using X3DXNA.utils;
using X3DXNA.core.CustomNodes;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Graphics;
using System.Windows.Forms;

namespace X3DXNA.core.Sensors
{
    class MyTouchSensor : IRoutable
    {

        #region Private Fields

        bool _isActive;
        bool _isOver;
        Vector3 _hitPoint_changed;
        Vector3 _hitNormal_changed;
        Vector3 _hitTexCoord_changed;
        SFTime _touchTime;

        bool shouldActivateTouch;
        List<MyShape> shapes;
        Core core;
        Vector3[] intersectedTriangle;

        #endregion

        #region Public Properties

        public String DEF { get; set; }
        public String description { get; set; }
        public bool enabled { get; set; }
        public bool isActive { get { return _isActive; } }
        public bool isOver { get { return _isOver; } }
        public Vector3 hitPoint_changed { get { return _hitPoint_changed; } }
        public Vector3 hitNormal_changed { get { return _hitNormal_changed; } }
        public Vector3 hitTexCoord_changed { get { return _hitTexCoord_changed; } }
        public SFTime touchTime { get { return _touchTime; } }

        public bool getShouldIActivateTouch { get { return shouldActivateTouch; } }
        public Vector3[] getIntersectedTriangle { get { return intersectedTriangle; } }

        #endregion

        #region Constructor

        public MyTouchSensor(Core core, String DEF, String description, bool enabled) 
        {
            this.DEF = DEF;
            this.description = description;
            this.enabled = enabled;
            this.shapes = new List<MyShape>();
            this.core = core;
            this.intersectedTriangle = new Vector3[3];
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Esegue un aggiornamento sul TouchSensor, controllando se il cursore del mouse interseca una delle shapes
        /// salvate nell'elenco.
        /// </summary>
        public void Update()
        {
            shouldActivateTouch = false;
            
            //Non ha senso eseguire un update se questo TouchSensor non ha shape che influenza.
            //TouchSensor inutile, andrebbe progettato un dispose.
            if (shapes.Count != 0)
            {
                //Controlliamo se una delle shapes presenti nella lista del TouchSensor viene intersecata
                //dal Ray attuale del mouse. Inizialmente eseguiamo un controllo grezzo sulla bounding.
                Tuple<float?, MyShape> intersection = getNearestIntersectedShape(shapes, core.getMouseRay());
                if (intersection != null){
                    //Ora eseguiamo un controllo più accurato sulla shape intersecata.
                    Matrix modelTransformation;

                    if (intersection.Item2.isDynamic)
                        modelTransformation = intersection.Item2.Geometry.getBasicTransformation * intersection.Item2.Trasformation.getMatrixTransformation;
                    else
                        modelTransformation = intersection.Item2.Geometry.World;

                    float? perTriangleDistance = RayIntersectsModel(core.getMouseRay(), intersection.Item2.Geometry, modelTransformation, out intersectedTriangle[0], out intersectedTriangle[1], out intersectedTriangle[2]);

                    //Se effettivamente c'è una intersezione (controllata "al triangolo")...
                    if (perTriangleDistance != null)
                    {
                        shouldActivateTouch = true;

                        //Infine controlliamo se qualche altra shape non sia in mezzo. (Controllo semplice tramite Bounding)
                        //Otteniamo la lista della Shapes dal core...
                        List<MyShape> shapesToCheck = core.getShapes();
                        //...ve ne rimuoviamo le shapes contenute nel TouchSensors...
                        foreach (MyShape shape in shapes)
                            shapesToCheck.Remove(shape);
                        //...e infine estraiamo le shapes che sono davanti al Touch.
                        Tuple<float?, List<MyShape>> otherShapesIntersection = rayIntersectAShapeInList(shapesToCheck, core.getMouseRay(), (float)intersection.Item1);
                        //Se esistono delle shapes davanti al Touch...

                        if (otherShapesIntersection != null)
                        {
                            //In questo momento ci troviamo nella situazione: il Ray ha intersecato una delle MyShape (SHAPE1) 
                            //nella lista del TouchSensor, inoltre ci sono delle altre MyShape le cui Bounding sono davanti
                            //a SHAPE1. Queste shapes sono salvate in "otherShapesIntersection". Occorre ora fare un controllo 
                            //più accurato, controllando, triangolo per triangolo, le shapes.

                            foreach (MyShape shape in otherShapesIntersection.Item2)
                            {
                                if (shape.isDynamic)
                                    modelTransformation = shape.Geometry.getBasicTransformation * shape.Trasformation.getMatrixTransformation;
                                else
                                    modelTransformation = shape.Geometry.World;

                                float? cuccuruccuccù = RayIntersectsModel(core.getMouseRay(), shape.Geometry, modelTransformation, out intersectedTriangle[0], out intersectedTriangle[1], out intersectedTriangle[2]);

                                if (cuccuruccuccù.HasValue)
                                {
                                    shouldActivateTouch = false;
                                    break;
                                }
                            }
                        }
                    }

                    if (shouldActivateTouch && core.getMouseLeftButton())
                        _touchTime = SFTime.Now;
                }
            }
        }

        /// <summary>
        /// Aggiunge una Shape alla lista delle Shapes che dovranno essere controllate dal TouchSensor.
        /// </summary>
        /// <param name="shape">La Shape da aggiungere.</param>
        public void addShape(MyShape shape)
        {
            shapes.Add(shape);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Controlla se una qualsiasi delle shapes passate come parametro interseca un Ray. Se qualche shape viene intersecata,
        /// viene restituita la lista di shape entro la distanza specificata.
        /// </summary>
        /// <param name="shapes">La lista di shapes da controllare.</param>
        /// <param name="ray">Il Ray.</param>
        /// <param name="distance">La distanza entro cui restituire le shape intersecate.</param>
        /// <returns>Ritorna una Tupla<float?, List<MyShape>> che rappresenta la distanza della shape intersecata più vicina
        /// (NULL se non viene intersecata alcuna shape) e la lista delle shape intersecate entro la distanza specificata.</returns>
        private Tuple<float?, List<MyShape>> rayIntersectAShapeInList(List<MyShape> shapes, Ray ray, float distance)
        {
            float? minDistance = float.MaxValue;
            List<MyShape> intersectedShapes = new List<MyShape>();

            foreach (MyShape shape in shapes)
            {
                float? distanceOfShape = shape.bounding.Intersects(ray);
                if (distanceOfShape.HasValue && distanceOfShape < distance)
                {
                    if (distanceOfShape < minDistance)
                        minDistance = distanceOfShape;
                    intersectedShapes.Add(shape);
                }
            }

            if (minDistance.Equals(float.MaxValue))
                return null;
            else
                return new Tuple<float?, List<MyShape>>(minDistance, intersectedShapes);
        }

        /// <summary>
        /// Ritorna la più distanza con la shape intersecata più vicina nell'elenco passato come parametro.
        /// </summary>
        /// <param name="shapes">Le shapes da controllare.</param>
        /// <param name="ray">Il Ray.</param>
        /// <returns>La distanza dela shape intersecata pià vicina. Null se non è stata trovata alcuna intersezione.</returns>
        private Tuple<float?, MyShape> getNearestIntersectedShape(List<MyShape> shapes, Ray ray)
        {
            float? minDistance = float.MaxValue;
            MyShape minIntersectedShape = null;

            foreach (MyShape shape in shapes)
            {
                float? distanceOfShape = shape.bounding.Intersects(ray);
                if (distanceOfShape.HasValue && distanceOfShape < minDistance)
                {
                    minDistance = distanceOfShape;
                    minIntersectedShape = shape;
                }
            }

            if (minDistance.Equals(float.MaxValue))
                return null;
            else
                return new Tuple<float?, MyShape>(minDistance, minIntersectedShape);
        }

        /// <summary>
        /// Checks whether a ray intersects a model. This method needs to access
        /// the model vertex data, so the model must have been built using the
        /// custom TrianglePickingProcessor provided as part of this sample.
        /// Returns the distance along the ray to the point of intersection, or null
        /// if there is no intersection.
        /// </summary>
        static float? RayIntersectsModel(Ray ray, IGeometry model, Matrix modelTransform,
                                         out Vector3 vertex1, out Vector3 vertex2,
                                         out Vector3 vertex3)
        {
            vertex1 = vertex2 = vertex3 = Vector3.Zero;

            // The input ray is in world space, but our model data is stored in object
            // space. We would normally have to transform all the model data by the
            // modelTransform matrix, moving it into world space before we test it
            // against the ray. That transform can be slow if there are a lot of
            // triangles in the model, however, so instead we do the opposite.
            // Transforming our ray by the inverse modelTransform moves it into object
            // space, where we can test it directly against our model data. Since there
            // is only one ray but typically many triangles, doing things this way
            // around can be much faster.

            Matrix inverseTransform = Matrix.Invert(modelTransform);

            ray.Position = Vector3.Transform(ray.Position, inverseTransform);
            ray.Direction = Vector3.TransformNormal(ray.Direction, inverseTransform);

            // Keep track of the closest triangle we found so far,
            // so we can always return the closest one.
            float? closestIntersection = null;

            // Loop over the vertex data, 3 at a time (3 vertices = 1 triangle).
            Vector3[] vertices = model.getTriangleVertices();

            for (int i = 0; i < vertices.Length; i += 3)
            {
                // Perform a ray to triangle intersection test.
                float? intersection;

                RayIntersectsTriangle(ref ray,
                                        ref vertices[i],
                                        ref vertices[i + 1],
                                        ref vertices[i + 2],
                                        out intersection);

                // Does the ray intersect this triangle?
                if (intersection != null)
                {
                    // If so, is it closer than any other previous triangle?
                    if ((closestIntersection == null) ||
                        (intersection < closestIntersection))
                    {
                        // Store the distance to this triangle.
                        closestIntersection = intersection;

                        // Transform the three vertex positions into world space,
                        // and store them into the output vertex parameters.
                        Vector3.Transform(ref vertices[i],
                                            ref modelTransform, out vertex1);

                        Vector3.Transform(ref vertices[i + 1],
                                            ref modelTransform, out vertex2);

                        Vector3.Transform(ref vertices[i + 2],
                                            ref modelTransform, out vertex3);
                    }
                }
            }

            return closestIntersection;
        }


        /// <summary>
        /// Checks whether a ray intersects a triangle. This uses the algorithm
        /// developed by Tomas Moller and Ben Trumbore, which was published in the
        /// Journal of Graphics Tools, volume 2, "Fast, Minimum Storage Ray-Triangle
        /// Intersection".
        /// 
        /// This method is implemented using the pass-by-reference versions of the
        /// XNA math functions. Using these overloads is generally not recommended,
        /// because they make the code less readable than the normal pass-by-value
        /// versions. This method can be called very frequently in a tight inner loop,
        /// however, so in this particular case the performance benefits from passing
        /// everything by reference outweigh the loss of readability.
        /// </summary>
        static void RayIntersectsTriangle(ref Ray ray,
                                          ref Vector3 vertex1,
                                          ref Vector3 vertex2,
                                          ref Vector3 vertex3, out float? result)
        {
            // Compute vectors along two edges of the triangle.
            Vector3 edge1, edge2;

            Vector3.Subtract(ref vertex2, ref vertex1, out edge1);
            Vector3.Subtract(ref vertex3, ref vertex1, out edge2);

            // Compute the determinant.
            Vector3 directionCrossEdge2;
            Vector3.Cross(ref ray.Direction, ref edge2, out directionCrossEdge2);

            float determinant;
            Vector3.Dot(ref edge1, ref directionCrossEdge2, out determinant);

            // If the ray is parallel to the triangle plane, there is no collision.
            if (determinant > -float.Epsilon && determinant < float.Epsilon)
            {
                result = null;
                return;
            }

            float inverseDeterminant = 1.0f / determinant;

            // Calculate the U parameter of the intersection point.
            Vector3 distanceVector;
            Vector3.Subtract(ref ray.Position, ref vertex1, out distanceVector);

            float triangleU;
            Vector3.Dot(ref distanceVector, ref directionCrossEdge2, out triangleU);
            triangleU *= inverseDeterminant;

            // Make sure it is inside the triangle.
            if (triangleU < 0 || triangleU > 1)
            {
                result = null;
                return;
            }

            // Calculate the V parameter of the intersection point.
            Vector3 distanceCrossEdge1;
            Vector3.Cross(ref distanceVector, ref edge1, out distanceCrossEdge1);

            float triangleV;
            Vector3.Dot(ref ray.Direction, ref distanceCrossEdge1, out triangleV);
            triangleV *= inverseDeterminant;

            // Make sure it is inside the triangle.
            if (triangleV < 0 || triangleU + triangleV > 1)
            {
                result = null;
                return;
            }

            // Compute the distance along the ray to the triangle.
            float rayDistance;
            Vector3.Dot(ref edge2, ref distanceCrossEdge1, out rayDistance);
            rayDistance *= inverseDeterminant;

            // Is the triangle behind the ray origin?
            if (rayDistance < 0)
            {
                result = null;
                return;
            }

            result = rayDistance;
        }


        #endregion

        #region Public Static Builder

        public static MyTouchSensor getNewTouchSensor(Core core, TouchSensor ts)
        {
            String DEF = "Standard TouchSensor";
            String description = "Standard TouchSensor";
            bool enabled = true;

            if (ts.DEF != null)
                DEF = ts.DEF;
            if (ts.description != null)
                description = ts.description;
            if (ts.enabled != null)
                enabled = utility.stringToBool(ts.enabled);

            return new MyTouchSensor(core, DEF, description, enabled);
        }

        #endregion

    }
}
